package com.vmj.heap;

import com.vmj.classfile.ClassReader;
import com.vmj.classfile.Classpath;
import com.vmj.classfile.entity.ClassFile;
import com.vmj.classfile.entity.ConstantInfo;
import com.vmj.classfile.entity.ConstantPool;
import com.vmj.classfile.entity.constantinfo.*;
import com.vmj.rtda.Slot;

import java.io.IOException;
import java.util.*;

/**
 * Created by yangshoujun on 2017/8/26 下午3:17.
 */
public class ClassLoader {

    private Classpath classpath;
    private Map<String, VMClass> classMap = new HashMap<String, VMClass>();

    public ClassLoader(Classpath classpath) {
        this.classpath = classpath;
        loadBasicClasses();
        loadPrimitiveClasses();
    }

    private void loadBasicClasses() {
        VMClass jlClassClass = loadClass("java/lang/Class");
        Collection<VMClass> classes = classMap.values();
        for(VMClass cls : classes) {
            if(cls.getjClass() == null) {
                cls.setjClass(jlClassClass.newObject());
                cls.getjClass().setExtra(cls);
            }
        }
    }

    private void loadPrimitiveClasses() {
        Collection<String> classes = ClassUtils.primitiveMap.keySet();
        for(String cls : classes) {
            loadPrimitiveClass(cls);
        }
    }

    private void loadPrimitiveClass(String className) {
        VMClass clazz = new VMClass();
        clazz.setAccessFlags(AccessFlag.ACC_PUBLIC);
        clazz.setName(className);
        clazz.setClassLoader(this);
        clazz.setInitStarted(true);
        clazz.setjClass(classMap.get("java/lang/Class").newObject());
        clazz.getjClass().setExtra(clazz);
        classMap.put(className, clazz);
    }

    public VMClass loadClass(String name) {
        if(classMap.containsKey(name)) {
            return classMap.get(name);
        }

        VMClass clazz = null;
        // 加载数组类
        if(name.charAt(0) == '[') {
            clazz = loadArrayClass(name);
        } else {
            clazz = loadNonArrayClass(name);
        }

        if(classMap.containsKey("java/lang/Class")) {
            VMClass jlClassClass = classMap.get("java/lang/Class");
            clazz.setjClass(jlClassClass.newObject());
            clazz.getjClass().setExtra(clazz);
        }
        return clazz;
    }

    private VMClass loadNonArrayClass(String name) {
        try {
            byte[] data = classpath.loadClass(name);
            VMClass clazz = defineClass(data);
            link(clazz);
            return clazz;
        } catch (IOException e) {
            throw new RuntimeException("java.lang.ClassNotFoundException", e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("java.lang.ClassNotFoundException", e);
        }

    }

    private VMClass loadArrayClass(String name) {
        VMClass clazz = new VMClass();
        clazz.setAccessFlags(AccessFlag.ACC_PUBLIC);
        clazz.setName(name);
        clazz.setClassLoader(this);
        clazz.setInitStarted(true);
        clazz.setSuperClass(loadClass("java/lang/Object"));
        clazz.setSuperClassName("java/lang/Object");
        clazz.setMethods(Collections.<Method>emptyList());

        List<VMClass> interfaces = new ArrayList<VMClass>();
        interfaces.add(loadClass("java/lang/Cloneable"));
        interfaces.add(loadClass("java/io/Serializable"));
        clazz.setInterfaces(interfaces);
        clazz.setInterfaceNames(Arrays.asList("java/lang/Cloneable", "java/io/Serializable"));
        classMap.put(name, clazz);

        return clazz;
    }

    private VMClass defineClass(byte[] data) {
        VMClass clazz = parseClass(data);
        classMap.put(clazz.getName(), clazz);
        return clazz;
    }

    private VMClass parseClass(byte[] data) {
        ClassReader reader = new ClassReader(data);
        ClassFile cf = reader.parse();
        ConstantPool cp = cf.getConstantPool();

        VMClass clazz = new VMClass();
        clazz.setAccessFlags(cf.getAccessFlag());
        clazz.setClassLoader(this);
        clazz.setName(cf.getThisClassName());
        clazz.setSourceFile(cf.getSourceFileName());

        RuntimeConstantPool runtimeConstantPool = RuntimeConstantPool.parseConstantPool(clazz, cp);
        clazz.setRuntimeConstantPool(runtimeConstantPool);
        // 解析成员变量
        clazz.setFields(Field.parseField(clazz, cf.getFields()));
        // 解析方法
        clazz.setMethods(Method.parseMethods(clazz, cf.getMethods()));
        // 设置接口名
        clazz.setInterfaceNames(cf.getInterfaceNames());
        // 设置父类名
        clazz.setSuperClassName(cf.getSuperClassName());
        clazz.setCp(cp);
        resolveSuperClass(clazz);
        resolveInterfaces(clazz);

        return clazz;
    }

    private void resolveSuperClass(VMClass clazz) {
        String name = clazz.getName();
        if(!"java/lang/Object".equals(name)) {
            clazz.setSuperClass(clazz.getClassLoader().loadClass(clazz.getSuperClassName()));
        }
    }

    private void resolveInterfaces(VMClass clazz) {
        List<VMClass> interfaces = new ArrayList<VMClass>();
        for(String name : clazz.getInterfaceNames()) {
            VMClass interfaceClazz = clazz.getClassLoader().loadClass(name);
            interfaces.add(interfaceClazz);
        }
        clazz.setInterfaces(interfaces);
    }

    private void link(VMClass clazz) {
        verify(clazz);
        prepare(clazz);
    }

    private void verify(VMClass clazz) {
        // TODO
    }

    private void prepare(VMClass clazz) {
        calcInstanceFieldSlotIds(clazz);
        calcStaticFieldSlotIds(clazz);
        allocAndInitStaticVars(clazz);
    }

    private void calcInstanceFieldSlotIds(VMClass clazz) {
        int slotId = 0;
        if(clazz.getSuperClass() != null) {
            slotId = clazz.getSuperClass().getInstanceSlotCount();
        }
        for(Field field : clazz.getFields()) {
            if(!field.isStatic()) {
                field.setSlotId(slotId);
                slotId++;
                if(field.isLongOrDouble()) {
                    slotId++;
                }
            }
        }
        clazz.setInstanceSlotCount(slotId);
    }

    private void calcStaticFieldSlotIds(VMClass clazz) {
        int slotId = 0;
        if(clazz.getSuperClass() != null) {
            slotId = clazz.getSuperClass().getInstanceSlotCount();
        }
        for(Field field : clazz.getFields()) {
            if(field.isStatic()) {
                field.setSlotId(slotId);
                slotId++;
                if(field.isLongOrDouble()) {
                    slotId++;
                }
            }
        }
        clazz.setStaticSlotCount(slotId);
    }

    private void allocAndInitStaticVars(VMClass clazz) {
        initStaticVars(clazz);
        for(Field field : clazz.getFields()) {
            if(field.isStatic() && field.isFinal()) {
                initStaticFinalVar(clazz, field);
            }
        }
    }

    private void initStaticFinalVar(VMClass clazz, Field field) {
        ConstantPool cp = clazz.getCp();
        int cpIndex = field.getConstValueIndex();
        int slotId = field.getSlotId();
        String descriptor = field.getDescriptor();
        Slots slots = clazz.getStaticVars();
        if(cpIndex > 0) {
            if(Arrays.asList("Z", "B", "C", "S", "I").contains(descriptor)) {
                ConstantInfo constantInfo = cp.getConstantInfos().get(cpIndex);
                ConstantIntegerInfo intConst = (ConstantIntegerInfo) constantInfo;
                int val = intConst.getValue();
                slots.setInt(slotId, val);
            } else if("J".equals(descriptor)) {
                ConstantInfo constantInfo = cp.getConstantInfos().get(cpIndex);
                ConstantLongInfo intConst = (ConstantLongInfo) constantInfo;
                long val = intConst.getValue();
                slots.setLong(slotId, val);
            } else if("F".equals(descriptor)) {
                ConstantInfo constantInfo = cp.getConstantInfos().get(cpIndex);
                ConstantFloatInfo intConst = (ConstantFloatInfo) constantInfo;
                float val = intConst.getValue();
                slots.setFloat(slotId, val);
            } else if("D".equals(descriptor)) {
                ConstantInfo constantInfo = cp.getConstantInfos().get(cpIndex);
                ConstantDoubleInfo intConst = (ConstantDoubleInfo) constantInfo;
                double val = intConst.getValue();
                slots.setDouble(slotId, val);
            } else if("Ljava/lang/String;".equals(descriptor)) {
                RuntimeConstantPool runtimeConstantPool = clazz.getRuntimeConstantPool();
                Constant stringInfo = runtimeConstantPool.getConstant(cpIndex);
                String str = (String) stringInfo.getValue();
                VMObject strObj = StringPool.getStringObject(clazz.getClassLoader(), str);
                slots.setRef(slotId, strObj);
            }
        }
    }

    private void initStaticVars(VMClass clazz) {
        int slotCount = clazz.getStaticSlotCount();
        Slot[] slotArr = new Slot[slotCount];
        Slots slots = new Slots(slotArr);
        clazz.setStaticVars(slots);
    }

    public Classpath getClasspath() {
        return classpath;
    }

    public Map<String, VMClass> getClassMap() {
        return classMap;
    }
}
